﻿using System;
using System.Collections.Generic;
using BizElements.Core;

namespace BizElements.BusinessLayer
{
    /// <summary>Builds journal table records using generated table metadata, i.e. by maping generated property names.</summary>
    /// <typeparam name="TSourceEntity">Source table entity class.</typeparam>
    /// <typeparam name="TJournalEntity">Journal table entity class.</typeparam>
    public class JournalEntityBuilder<TSourceEntity, TJournalEntity> : IJournalEntityBuilder<TSourceEntity, TJournalEntity>
        where TSourceEntity : IEntity, new()
        where TJournalEntity : IEntity, new()
    {
        IDictionary<string, string> propertyMap;

        /// <summary>Initializes a new instance of JournalEntityBuilder class.</summary>
        public JournalEntityBuilder()
        {
            this.JournalTimeStampPrecision = TimeStampPrecision.Maximum;
        }

        /// <summary>Builds journal record for the specified source entity.</summary>
        /// <param name="entity">Entity whose properties will be journaled.</param>
        /// <returns>Journal table entity.</returns>
        public TJournalEntity BuildJournalEntity(TSourceEntity entity)
        {
            if (this.propertyMap == null)
                throw new InvalidOperationException("Property map must be created before journal record can be created.");

            DateTime timeStamp = RoundTimeStamp(Time.Now, this.JournalTimeStampPrecision);
            TJournalEntity journalEntity = BuildJournalEntity(entity, timeStamp);
            return journalEntity;
        }

        private TJournalEntity BuildJournalEntity(TSourceEntity entity, DateTime timeStamp)
        {
            TJournalEntity journalEntity = new TJournalEntity();
            if (this.JournalTimeStampProperty != null)
            {
                IDbColumn timeStampField = journalEntity.Table.Columns.GetByPropertyName(this.JournalTimeStampProperty);
                journalEntity.SetField(timeStampField, timeStamp);
            }

            foreach (KeyValuePair<string, string> sourceAndJournalProperty in this.propertyMap)
            {
                IDbColumn sourceField = entity.Table.Columns.GetByPropertyName(sourceAndJournalProperty.Key);
                IDbColumn journalField = journalEntity.Table.Columns.GetByPropertyName(sourceAndJournalProperty.Value);

                object fieldValue = entity.GetField(sourceField);
                journalEntity.SetField(journalField, fieldValue);
            }
            return journalEntity;
        }

        /// <summary>Builds journal record for the specified source entity.</summary>
        /// <param name="entities">Entities whose properties will be journaled.</param>
        /// <returns>Journal table entities.</returns>
        public IEntityCollection BuildJournalEntityCollection(IEnumerable<TSourceEntity> entities)
        {
            if (this.propertyMap == null)
                throw new InvalidOperationException("Property map must be created before journal record can be created.");

            DateTime timeStamp = RoundTimeStamp(Time.Now, this.JournalTimeStampPrecision);
            IEntityCollection journalEntities = new EntityCollection(new TSourceEntity().Table);
            foreach (TSourceEntity sourceEntity in entities)
            {
                TJournalEntity journalEntity = BuildJournalEntity(sourceEntity, timeStamp);
                journalEntities.Add(journalEntity);
            }

            return journalEntities;
        }

        /// <summary>Maps source entity to journal entity property.</summary>
        /// <param name="sourceProperty">Source entity property name.</param>
        /// <param name="journalProperty">Journal table entity property name.</param>
        public void MapProperty(string sourceProperty, string journalProperty)
        {
            if (this.propertyMap == null)
                this.propertyMap = new Dictionary<string, string>();

            this.propertyMap[sourceProperty] = journalProperty;
        }

        /// <summary>Automatically maps source entity properties to journal table entity properties. Maps properties with equal names. Data type is not checked.</summary>
        public void CreateDefaultPropertyMap()
        {
            if (this.propertyMap == null)
                this.propertyMap = new Dictionary<string, string>();

            this.propertyMap.Clear();
            IDbTable sourceTable = new TSourceEntity().Table;
            IDbTable journalTable = new TJournalEntity().Table;
            foreach (IDbColumn sourceField in sourceTable.Columns)
            {
                string propertyName = sourceField.PropertyName;
                if (journalTable.Columns.GetByPropertyName(propertyName) != null)
                    this.propertyMap[propertyName] = propertyName;
            }
        }

        /// <summary>Optional time-stamp property in journal table entities.</summary>
        public string JournalTimeStampProperty { get; set; }

        /// <summary>Time-stamp precision for optional time-stamp property in journal table. Default is <see cref="TimeStampPrecision.Maximum"/>.</summary>
        public TimeStampPrecision JournalTimeStampPrecision { get; set; }

        private static DateTime RoundTimeStamp(DateTime timeStamp, TimeStampPrecision precision)
        {
            switch (precision)
            {
                case TimeStampPrecision.Day:
                    return timeStamp.Date;

                case TimeStampPrecision.Minute:
                    return RoundToMinutes(timeStamp);

                case TimeStampPrecision.Second:
                    return RoundToSeconds(timeStamp);

                case TimeStampPrecision.Maximum:
                    return timeStamp;

                default:
                    throw new ArgumentException("Unsupported time-stamp precision: " + precision.ToString(), "precision");
            }
        }

        private static DateTime RoundToMinutes(DateTime timeStamp)
        {
            return new DateTime(timeStamp.Year, timeStamp.Month, timeStamp.Day, timeStamp.Hour, timeStamp.Minute, 0);
        }

        private static DateTime RoundToSeconds(DateTime timeStamp)
        {
            return new DateTime(timeStamp.Year, timeStamp.Month, timeStamp.Day, timeStamp.Hour, timeStamp.Minute, timeStamp.Second);
        }
    }    
}
